{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\"></center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center>Открытый курс по машинному обучению. Сессия № 3</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### <center>Индивидуальный проект по анализу данных</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center>Прогнозирование пристройства домашних животных</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<left><i>автор: Осипова Зоя</i></left>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Общее описание проблемы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проблема потерявшихся и бездомных животных стоит особенно остро в крупных городах. Актуальной задачей является поиск хозяев, социализация, и в случае не нахождения оных, пристройство животного в новые руки.\n",
    "Наиболее близким переводом на русский, отражающим деятельность центра, думаю, будет термин \"приют для животных\", который осуществляет поиск новых или старых хозяев, а также привлечение добровольцев для временной передержки и помощи. Данной центр является самым большим в США приютом для животных, проводящий политику \"no-kill\" (животные не усыпляются по прошествии какого-то времени нахождения в приюте)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Данные"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наборы данных взят непосредственно с сайта центра [Austin Animal Center](http://www.austintexas.gov/department/aac), 19 апреля 2018 года (датасет обновляется ежедневно и ведется с 2013 года).\n",
    "Данные были доступны по ссылкам на сайте центра: [раз](https://data.austintexas.gov/Health-and-Community-Services/Austin-Animal-Center-Intakes/wter-evkm) и [два](https://data.austintexas.gov/Health-and-Community-Services/Austin-Animal-Center-Outcomes/9t4d-g238), если не получается скачать (у меня несколько последних дней не открывались, возможно из-за РКН), то Яндекс-диск: [Outcomes](https://yadi.sk/i/USIwAMaR3UeosH) и [Intakes](https://yadi.sk/i/1J6Bher-3Ueoud)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Признаки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Датасет содержит информацию о более чем 80 тысяч животных, присутствуют несколько категорий - собаки, кошки, птицы и остальные.\n",
    "Есть данные о возрасте, имени (если есть), времени поступлении в приют, и так далее. Данные содержатся в двух таблицах, рассмотрим подробнее:\n",
    "\n",
    "<b>Таблица outcomes:</b>\n",
    "- <b>Animal ID (animal_id)</b> - идентификатор животного\n",
    "- <b>Name (animal_id)</b> - имя животного\n",
    "- <b>DateTime (outcome_time)</b> - дата \"выпуска\" из центра\n",
    "- <b>MonthYear (outcome_monthyear)</b> - месяц-год выпуска\n",
    "- <b>Date of Birth (date_of_birth)</b> - дата рождения\n",
    "- <b>Animal Type (animal_type)</b> - вид животного (кошка, собака, птица и т.д.)\n",
    "- <b>Sex upon Outcome (outcome_sex)</b> - пол на момент выпуска (может меняться, так как кроме женского/мужского присутствуют указания кастрированно/стерилизованно ли животное)\n",
    "- <b>Age upon Outcome (outcome_age)</b> - возраст на момент выпуска\n",
    "- <b>Breed (breed)</b> - порода\n",
    "- <b>Color (color)</b> - цвет\n",
    "- <b>Outcome Type (outcome_type)</b> - интересующая нас целевая переменная, принимает 9 значений: Adoption, Died, Euthanasia, Disposal, Missing, Rto-adopt, Relocate. Тут в основном все понятно, единственное, затрудняюсь сказать что такое Rto-adopt, возможно повторное возвращение к человеку, однажды забравшему животного.\n",
    "- <b>Outcome Subtype (outcome_subtype)</b> - пояснения к Outcome Type, полупустой столбец\n",
    "\n",
    "\n",
    "<b>Таблица intakes:</b>\n",
    "Часть данных повторяется, помимо этого:\n",
    "- <b>Intake Type (intake_type)</b> - как поступило животное (найдено в дикой природе, кто-то привез, и т.д.)\n",
    "- <b>Intake Condition (intake_condition)</b> - в каком состоянии найдено животное\n",
    "- <b>DateTime (intake_time)</b> - дата поступления\n",
    "- <b>MonthYear (intake_monthyear)</b> - месяц-год поступления\n",
    "- <b>Age upon Intake (intake_age)</b> - возраст на момент поступления\n",
    "- <b>Found Location (found_location)</b> - где найдено\n",
    "- <b>Sex upon Intake (intake_sex)</b> - пол на момент поступления"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Цели и задачи"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Задачей данного проекта является поиск закономерностей и прогнозирование судьбы животных, попавших в приют. Понимание того, от каких признаков зависит пристройство животного, во-первых, могло бы позволить центру лучше прогнозировать ресурсы, во-вторых, возможно, проводить какие-то социальные кампании."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### Импорт библиотек"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "%matplotlib inline\n",
    "from matplotlib import pyplot as plt\n",
    "import seaborn as sns\n",
    "import os\n",
    "import re\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.metrics import classification_report, f1_score, make_scorer\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder, FunctionTransformer\n",
    "from sklearn.model_selection import GridSearchCV, train_test_split, StratifiedKFold, validation_curve, learning_curve\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "\n",
    "sns.set_style('whitegrid')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Загружаем данные из таблицы Outcomes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes = pd.read_csv('Austin_Animal_Center_Outcomes.csv', sep=',', index_col=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Переименуем сразу столбцы для более удобного обращения к таблице."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.rename(columns={'Animal ID': 'animal_id', 'Name': 'name', 'DateTime':'outcome_date', 'MonthYear':'outcome_monthyear', \\\n",
    "                        'Date of Birth':'date_of_birth','Outcome Type':'outcome_type', 'Outcome Subtype':'outcome_subtype', \\\n",
    "                        'Animal Type':'animal_type','Sex upon Outcome':'outcome_sex','Age upon Outcome':'outcome_age', \\\n",
    "                        'Breed':'breed', 'Color':'color'}, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на первые строчки таблицы и простую статистику по данным, а также на число уникальных значений в признаках."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.head(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.info()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.describe(include = ['object', 'int64', 'float64'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение целевой переменной по категориям:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes['outcome_type'].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что целевая переменная не сбалансирована по классам."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Предварительные наблюдения:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " - Данные категориальные, причем количество значений некоторых признаков очень велико (~2 тысяч для признака 'breed', например).\n",
    " - Распределение по классам в целевом признаке \"outcome_type\" несбалансированное.\n",
    " - Видим, что столбец 'outcome_monthyear' дублирует 'date', преобразуем и переименуем, остальные временные признаки приведем их к временному формату.\n",
    " - Столбец 'Outcome Subtype' содержит около 54% пропусков, от него лучше избавиться (к тому же учитывая, что в этом столбце содержатся в основном уточнения к 'outcome_type', например 'Suffering' при 'Euthanasia'). \n",
    " - Также много пропусков в 'name', но сам признак того, что у животного есть имя кажется мне полезным. Cоздадим бинарный признак 'is_name'.\n",
    " - Небольшое количество пропусков есть в 'outcome_sex', 'outcome_age' и 'outcome_type', здесь строчки с пропуском выкинем.\n",
    " - По количеству уникальных значений (73422 из 81322) столбца animal_id видно, что около 9.7% животных попадают в центр повторно."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Работаем с временными признаками: приводим даты к временному формату, исправляем и переименовываем \"MonthYear\", сортируем по дате:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes['outcome_date'] = outcomes['outcome_date'].apply(pd.to_datetime)\n",
    "outcomes['date_of_birth'] = outcomes['date_of_birth'].apply(pd.to_datetime)\n",
    "outcomes = outcomes.sort_values(by='outcome_date')\n",
    "outcomes['outcome_monthyear'] = outcomes['outcome_date'].apply(lambda x: x.year*100+x.month)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выкидываем столбец 'outcome_subtype', заполняем пропуски в именах и создаем бинарный признак is_name, выкидываем строчки с пропущенными значениями в 'outcome_sex', 'outcome_age' и 'outcome_type':"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcomes.drop(['outcome_subtype'], axis=1, inplace=True)\n",
    "outcomes['name'] = outcomes['name'].fillna('Unknown').astype(str)\n",
    "outcomes['is_name'] = outcomes['name'].apply(lambda x: 1 if x != 'Unknown' else 0)\n",
    "outcomes.dropna(inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Удалим все записи с повторяющимся animal_id, предварительно создав колонку с признаком is_uniq (по умолчанию drop_duplicates оставляет первую из дублированных записей, а так как мы отсортировали таблицу по дате, это будет запись о первом пристройстве животного):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# проверям animal_id на повторы, параметр keep=False означает что мы ищем все записи, встречающиеся больше одного раза\n",
    "# (если оставить keep по умолчанию, то первая запись из дублированных не будет отмечаться как True)\n",
    "outcomes['is_uniq'] = outcomes['animal_id'].duplicated(keep=False).map({False:1, True:0})\n",
    "\n",
    "# выбрасываем дублированные записи кроме первого встреченного раза\n",
    "outcomes.drop_duplicates(['animal_id'], inplace=True, keep='first')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Использование дополнительных данных из таблицы Intakes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Помимо таблицы Outcomes, в нашем распоряжении есть данные о поступлении животных в центр, посмотрим, сможем ли мы взять что-нибудь полезное из нее."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "intakes = pd.read_csv('Austin_Animal_Center_Intakes.csv', sep=',', index_col=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также переименуем columns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "intakes.rename(columns={'Animal ID': 'animal_id', 'Name': 'name', 'DateTime':'intake_date', 'MonthYear':'intake_monthyear', \\\n",
    "                        'Date of Birth':'date_of_birth','Intake Type':'intake_type', 'Intake Condition':'intake_condition', \\\n",
    "                        'Animal Type':'animal_type','Sex upon Intake':'intake_sex','Age upon Intake':'intake_age', \\\n",
    "                        'Breed':'breed', 'Color':'color', 'Found Location':'found_location'}, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "intakes.head(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем понять, какая информация, которой не было в первой таблице, интересна. На первый взгляд кажется полезной информация о времени, когда животное поступило в центр, о месте где его нашли, состоянии и типе (Type и Condition) на момент поступления, поле (кастрированный/стерилизованный на момент поступления или нет), возрасте. Объединим таблицы, взяв из данных о поступлении интересные нам и предварительно выкинув дубликаты и переименовав/преобразовав данные о дате."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "intakes['intake_date'] = intakes['intake_date'].apply(pd.to_datetime)\n",
    "intakes = intakes.sort_values(by='intake_date')\n",
    "intakes['intake_monthyear'] = intakes['intake_date'].apply(lambda x: x.year*100+x.month)\n",
    "intakes.drop_duplicates(['animal_id'], inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = outcomes.merge(intakes[['animal_id', 'intake_condition', 'intake_type', 'found_location', 'intake_sex',\\\n",
    "                               'intake_monthyear', 'intake_date', 'intake_age']], how='inner', left_on='animal_id', right_on='animal_id')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данные о точном времени поступления, рождения и пристройстве оставим, возможно они еще пригодятся для генерирования новых признаков. Создаем списки признаков по категориям для более удобной работы:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat_features = ['name', 'color','breed','animal_type','outcome_sex', \\\n",
    "                'intake_condition', 'intake_type', 'found_location', 'intake_sex',]\n",
    "\n",
    "month_features = ['outcome_monthyear', 'intake_monthyear']\n",
    "\n",
    "time_features = ['outcome_date','date_of_birth', 'intake_date']\n",
    "\n",
    "age_features = ['outcome_age', 'intake_age']\n",
    "\n",
    "bin_features = ['is_name','is_uniq']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Распределение целевой переменной"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,6))\n",
    "\n",
    "ax = sns.countplot(data['outcome_type'])\n",
    "ax.set_xlabel('Outcome Type', fontsize=18)\n",
    "ax.set_ylabel('Count', fontsize=18)\n",
    "ax.set_yticklabels(ax.get_yticklabels(), rotation=0)\n",
    "plt.show;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что распределение классов целевой переменной сильно несбалансированное."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Распределение временных признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,6))\n",
    "data['date_of_birth'].value_counts().sort_values().plot.line();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,6))\n",
    "data['outcome_date'].value_counts().resample('D').sum().plot.line();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,6))\n",
    "data['intake_date'].value_counts().resample('D').sum().plot.line();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ясно видны годовые пики активности в деятельности центра."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Анализ категориальных признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сначала посмотрим на распределения бинарных и категориальных признаков (с числом категорий < 10) и целевой переменной:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_cat(feature, loc='best', yscale='linear'):\n",
    "    \n",
    "    plt.figure(figsize=(12,6))    \n",
    "    plt.xlabel(feature, fontsize=12)\n",
    "    ax = sns.countplot(data[feature], hue=data['outcome_type']) \n",
    "    ax.set(yscale=yscale)\n",
    "    ax.legend(loc=loc)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('intake_sex')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('outcome_sex')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из этих двух предыдущих графиков видно, что большую часть поступающих животных стерилизуют (Это обычная практика в приютах/центрах для животных)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('intake_condition', loc=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что больные и раненные животные чаще подвергаются эвтаназии, чем найденные в здоровом состоянии."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('intake_type')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "К сожалению, почти всех поступивших диких животных усыпляют."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('animal_type', loc=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Интересное наблюдение: кошек гораздо реже, чем собак, возвращают владельцам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('is_uniq', loc=2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cat('is_name')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Животных с именами гораздо чаще возвращают владельцу (интерпретируемо, так как если известно имя, то скорее всего на кошке или собаке есть медальон с именем и адресом хозяина, или просто известно чье это животное). Что интереснее - животных с именами чаще и пристраивают новым владельцам. (Вешайте медальоны с адресом на кошек и собак!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на распределения топ-10 пород, цвета и места где нашли, в зависимости от целевой переменной:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "outcome_types = list(set(data['outcome_type'].values))\n",
    "\n",
    "def find_top10(feature):\n",
    "    \n",
    "    out_dict={}    \n",
    "    for i in outcome_types:\n",
    "        out_dict[i] = list(data[data['outcome_type'] == i][feature].value_counts().head(10).keys())\n",
    "        \n",
    "    return out_dict\n",
    "\n",
    "\n",
    "def plot_top10(feature):\n",
    "    \n",
    "    for idx, outcome in enumerate(outcome_types):\n",
    "        top_feature_list = find_top10(feature)[outcome]\n",
    "        data_x = data[data[feature].apply(lambda x: x in top_feature_list)][data['outcome_type']==outcome][feature]\n",
    "        order=data_x.value_counts().index\n",
    "        \n",
    "        plt.figure(figsize=(16,4))\n",
    "        plt.xticks(rotation=75, fontsize=12)\n",
    "        ax = 'ax{}'.format(idx)\n",
    "        ax = sns.countplot(data_x, order=order)\n",
    "        ax.set_title(outcome, fontsize=12)\n",
    "        ax.set_xlabel(xlabel='')\n",
    "        #ax.tick_params(rotation=75, labelsize=12)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_top10('breed')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В топе возвращаемых владельцу - породы собак, диких животных в основном выпускают или подвергают эвтаназии (видимо, пораненных), неоижданно в топе пристраиваемых и умерших порода кошки \"обычная домашняя\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_top10('color')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Каких-то особо интересных закономерностей нет, кроме того, что везде преобладают окрасы черный, черно-белый и коричневый."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_top10('found_location')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тоже особо интересного ничего нет, кроме того, что почти всех домашних животных находят в городе Austin."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_top10('outcome_age')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Интересные закономерности, пристраивают в основном щенков и котят, возвращают взрослых животных."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Особенностью данных являются разнородные признаки с преобладанием категориальных, причем с большим количеством значений. Наблюдаются некоторые зависимости между признаками и целевой переменной, например, животных с именами забирают больше, найденных диких больше подвергают эвтаназии, и так далее. Однако, то, что кастрированные и стерилизованные животные составляют большинство при пристройстве, означает лишь что, что большинство животных попавших в приют, как известно, стерилизуют, так что не все зависимости полезны."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "У нас задача многоклассовой классификации, причем с несбалансированными классами. Accuracy (просто доля верных ответов) сразу отпадает, так как нам будут важны результаты классификации по различным классам. Это удобно смотреть по таблице <b>classification report</b>, которая выводит результаты <b>presicion</b> (точность, насколько точно класс отделяется от других), <b>recall</b> (полнота, насколько хорошо, т.е \"полно\" мы находим этот класс) и <b>f1 меру</b> (среднее гармоническое между точностью и полнотой) по всем классам. Вот f1 меру и будем использовать (с микроусреднением по классам из-за несбалансированности целевой переменной) для измерения результатов моделей, периодически сверяясь с classification report."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Особенностью данных является разные категориальные признаки, соотвественно нам подойдут модели, которые умеют работать с категориальными признаками, и возможно, с большим количеством значений. Я думаю, нам подойдет случайный лес, возможно, линейные модели и градиентный бустинг. Выберем случайный лес за его простоту, качество и интерпретируемость."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Первым делом, преобразуем данные о возрасте.\n",
    "\n",
    "Ясно видно, что возраст животных в 'outcome_age' и 'intake_age' дан приблизительно, да и к тому же в разных единицах измерения (2 weeks, 1 year и так далее), и является на данный момент категориальными и неуопрядоченным. (Модель не будет \"знать\" что месяц меньше года и т.д). Для более точного анализа будет лучше вычислить возраст напрямую (на момент пристройства животного), используя данные в столбце 'date_of_birth'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['outcome_age'] = round((data['outcome_date'] - data['date_of_birth'])/np.timedelta64(1,'W'),2)\n",
    "data['intake_age'] = round((data['intake_date'] - data['date_of_birth'])/np.timedelta64(1,'W'),2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим, что получилось:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['outcome_age'].describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['intake_age'].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Есть записи с отрицательным возрастом, видимо, в записях о дне рождения были ошибки, заменим такие записи нулем:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data[data['outcome_age']<0]['outcome_age'] = 0\n",
    "data[data['intake_age']<0]['intake_age'] = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Выделим и преобразуем целевой признак"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "target = data['outcome_type']\n",
    "\n",
    "map_dir = {'Adoption':0, 'Died':1, 'Disposal':2, 'Euthanasia':3, 'Missing': 4, \\\n",
    "           'Relocate':5,'Return to Owner':6, 'Rto-Adopt':7, 'Transfer':8}\n",
    "\n",
    "map_rev = {0:'Adoption', 1:'Died', 2:'Disposal', 3:'Euthanasia', 4:'Missing', \\\n",
    "           5:'Relocate', 6:'Return to Owner', 7:'Rto-Adopt', 8:'Transfer'}\n",
    "\n",
    "y_ = target.map(map_dir)\n",
    "y = y_.values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_.value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Категориальные признаки преобразуем с помощью LabelEncoder, так как лес не любит слишком много признаков (а их получится много если будем использовать технику One Hot Encoder)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def lab_encoder(df, columns):    \n",
    "    for col in columns:\n",
    "        label_encoder = LabelEncoder()\n",
    "        df[col] = label_encoder.fit_transform(df[col])\n",
    "    return df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_cat = data[cat_features].copy()\n",
    "data_le = lab_encoder(data_cat, cat_features)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Соединим преобразованные признаки с бинарными и с возрастом в неделях."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_rf = pd.concat([data_le, data[age_features], data[bin_features], data[month_features]], axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разделим выборки на обучающую и отложенную, так как классы не сбалансированы, будем использовать параметр stratify:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_rf, X_holdout_rf, y_train_rf, y_holdout_rf = train_test_split(data_rf, y, test_size=0.3,\n",
    "                                                    random_state=17, stratify=y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Обучение, кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В параметрах случайного леса укажем class_weight='balanced'. Для оценивания качества модели будем использовать f1_score, для этого создадим scorer из metrics.f1_score, укажем микроусреднение по классам. При разбиенни по фолдам в кроссвалидации будем учитывать дисбаланс классов с помощью StratifiedKFold."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rf = RandomForestClassifier(class_weight='balanced')\n",
    "\n",
    "skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=17)\n",
    "f1_scorer = make_scorer(f1_score, average='micro')\n",
    "rf_params={'n_estimators':[100, 150, 300], 'min_samples_leaf':[2,3,5]}\n",
    "\n",
    "grid_rf = GridSearchCV(rf, rf_params, n_jobs=-1, cv=skf, verbose=1, scoring=f1_scorer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "grid_rf.fit(X_train_rf, y_train_rf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print ('Best score: ' , grid_rf.best_score_)\n",
    "print ('Best params: ' , grid_rf.best_params_)\n",
    "print ('Test std mean: ' , np.array(grid_rf.cv_results_['std_test_score']).mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на важность признаков с точки зрения леса:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feat_importance = pd.DataFrame(X_train_rf.columns, columns = ['features'])\n",
    "feat_importance['value'] = grid_rf.best_estimator_.feature_importances_\n",
    "feat_importance.sort_values('value')[::-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<b>Признак  \"время пребывания  в приюте\"</b>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выше мы видели, что временные признаки intake_month и outcome_month важны, попробуем скомбинировать их создав новый признак."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['time_in'] = round((data['outcome_date'] - data['intake_date'])/np.timedelta64(1,'D'),2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим (вдруг опять ошибки в данных):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['time_in'].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заменим кривые значения нулями:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data[data['time_in']<0]['time_in'] = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<b>Признак цвет + порода</b>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Предположение: так как цвет и порода неплохо оцениваются моделью, сделаем на основе этих двух признаков новый."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['color_breed'] = data['color'] + ' ' + data['breed']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<b>Признак \"есть mix в названии породы\", признак \"помесь двух пород\"</b>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Интуитивное предположение: может быть, для пристройства важна чистопородность кошки или собаки. Эту информацию можно извлечь из колонки 'breed': Mix - не чистопородное животное, запись двух пород через слэш - помесь этих двух пород."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['is_mix'] = data['breed'].apply(lambda x: 1 if 'mix' in x.lower() else 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['crossbreed'] = data['breed'].apply(lambda x: 1 if '/' in x else 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поссмотрим, что получилось:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.sample(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Обучим модель с новыми данными"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "columns = cat_features + ['color_breed']\n",
    "data_le_new = lab_encoder(data, columns)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_rf_new = pd.concat([data_le_new[columns], data['crossbreed'], data['is_mix'], data['time_in'], \\\n",
    "                         data['outcome_monthyear'], data['outcome_age'], data[bin_features]], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_holdout, y_train, y_holdout = train_test_split(data_rf_new, y, test_size=0.3,\n",
    "                                                    random_state=17, stratify=y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rf = RandomForestClassifier(class_weight='balanced')\n",
    "\n",
    "skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)\n",
    "f1_scorer = make_scorer(f1_score, average='micro')\n",
    "rf_params={'n_estimators':[100, 150, 300], 'min_samples_leaf':[2,3,5]}\n",
    "\n",
    "grid_rf = GridSearchCV(rf, rf_params, n_jobs=-1, cv=skf, verbose=1, scoring=f1_scorer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "grid_rf.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print ('Best score: ' , grid_rf.best_score_)\n",
    "print ('Best params: ' , grid_rf.best_params_)\n",
    "print ('Test std mean: ' , np.array(grid_rf.cv_results_['std_test_score']).mean())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feat_importance = pd.DataFrame(X_train.columns, columns = ['features'])\n",
    "feat_importance['value'] = grid_rf.best_estimator_.feature_importances_\n",
    "feat_importance.sort_values('value')[::-1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, качество модели на кросс-валидации подросло! С новым признаком \"время, проведенное в приюте\", мы также угадали. Также сочетание 'цвет + порода' неплох (можно было бы попробовать использовать на этом признаке TfIdf). А вот дворняги это, помеси или чистопородные, не очень важно."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Построение кривых валидации и обучения "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_err(x, data, **kwargs):\n",
    "    mu, std = data.mean(1), data.std(1)\n",
    "    lines = plt.plot(x, mu, '-', **kwargs)\n",
    "    plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "                     facecolor=lines[0].get_color(), alpha=0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<b>Построим кривые валидации</b>, будем менять сложность модели изменяя параметры."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f1_scorer = make_scorer(f1_score, average='micro')\n",
    "rf_val = RandomForestClassifier(class_weight='balanced', random_state=17)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на зависимость от числа деревьев."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n = np.linspace(50, 500, 5).astype(int)\n",
    "\n",
    "val_train, val_test = validation_curve(rf_val, X_train, y_train,\n",
    "                                       'n_estimators', n, cv=skf,\n",
    "                                       scoring=f1_scorer, n_jobs=-1)\n",
    "\n",
    "plot_with_err(n, val_train, label='training scores')\n",
    "plot_with_err(n, val_test, label='validation scores')\n",
    "plt.xlabel('n_estimators'); plt.ylabel('f1_score')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на зависимость от числа объектов в листе."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n = np.linspace(1, 10, 10).astype(int)\n",
    "\n",
    "val_train, val_test = validation_curve(rf_val, X_train, y_train,\n",
    "                                       'min_samples_leaf', n, cv=skf,\n",
    "                                       scoring=f1_scorer, n_jobs=-1)\n",
    "\n",
    "plot_with_err(n, val_train, label='training scores')\n",
    "plot_with_err(n, val_test, label='validation scores')\n",
    "plt.xlabel('min_samples_leaf'); plt.ylabel('f1_score')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Усложнение модели не приводит к росту качества."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<b>Построим обучающие кривые.</b>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_learning_curve(min_samples_leaf=2, n_estimators=300):\n",
    "    train_sizes = np.linspace(0.05, 1, 20)\n",
    "    \n",
    "    rf_learn = RandomForestClassifier(class_weight='balanced', min_samples_leaf=min_samples_leaf, n_estimators = n_estimators)\n",
    "    N_train, val_train, val_test = learning_curve(rf_learn, X_train, y_train, train_sizes=train_sizes, cv=skf,\n",
    "                                                  scoring=f1_scorer, n_jobs=-1)\n",
    "    plot_with_err(N_train, val_train, label='training scores')\n",
    "    plot_with_err(N_train, val_test, label='validation scores')\n",
    "    plt.xlabel('Training Set Size'); plt.ylabel('f1 score')\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_learning_curve()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим рост на кросс-валидации при увеличении датасета, возможно новые данные помогут."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сделаем оценку на отложенной выборке, используя лучшую модель из предыдущего шага."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "best_rf = grid_rf.best_estimator_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_predict = best_rf.predict(X_holdout)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "report = classification_report(y_holdout, y_predict)\n",
    "print(report, '\\n', map_rev)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, модель все-таки не очень хорошо различает малочисленные классы, но качество на отложенной выборке хорошее."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Оценка модели с описанием выбранной метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Итак, мы видим, что хоть модель и старалась оптимизировать f1 score, малочисленные классы (потерявшиеся животные, выпущенные в другом месте и возвращенные владельцу взявшему на адаптацию) она различает очень плохо. Вообще, с точки зрения применения, нам важнее всего предсказать, найдутся ли у данного животного хозяева, или возьмут ли его в новую семью. Возможно, удастся улучшить результат, использую данные не только о породе, но и о размере, пушистости, характере, и так далее, часть этих данных тяжело, но возможно, извлекается из данных о породе."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данные для исследования не очень чистые, много пропусков, дубликатов и неверных значений. На основе вычищенных данных построена модель для многоклассовой классификации (пристройства, возврата, и т.д) животного из приюта. Конкретно пристройство и возврат владельцу она предсказывает неплохо, но хотелось бы улучшить качество на малочисленных классах, возможно использование более интересных признаков поможет."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
